﻿namespace HyperLinq.Abstract
{
	using System;
	using System.Collections.Generic;
	using System.Linq;

	public static class ArrayExtensions
    {
		/// <summary>
		/// Generates a sequence of multidimensional indexes within a specified range.
		/// </summary>
		/// <param name="lengths"></param>
		/// <returns></returns>
		public static IEnumerable<int[]> Range(int[] lengths)
		{
			if (lengths == null)
				throw new ArgumentNullException("lengths");

			if (lengths.Any(i => i <= 0))
				throw new ArgumentException("lengths");

			// check equality of elements
			int[] cursor = new int[lengths.Length];
			cursor[lengths.Length - 1] = 0;

			while (cursor[0] < lengths[0])
			{
				// yield
				yield return cursor;

				// advance cursor
				cursor[lengths.Length - 1]++;

				for (int i = lengths.Length - 1; i > 0; i--)
				{
					if (cursor[i] >= lengths[i])
					{
						cursor[i - 1]++;
						cursor[i] = 0;
					}
				}
			}
		}

		/// <summary>
		/// Gets the length of all dimensions of an array at once.
		/// </summary>
		/// <param name="source"></param>
		/// <returns></returns>
		public static int[] GetLengths(this Array source)
		{
			if (source == null)
				throw new ArgumentNullException("source");

			int rank = source.Rank;
			int[] result = new int[rank];

			for (int i = 0; i < rank; i++)
				result[i] = source.GetLength(i);

			return result;
		}

		#region Conversions
		/// <summary>
		/// Returns the array as an enumerable.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="array"></param>
		/// <returns></returns>
		public static IEnumerable<T> AsEnumerable<T>(this Array array)
		{
			return new MultidimensionalArrayEnumerable<T>(array);
		}
		#endregion

		#region Maping
		/// <summary>
		/// Projects each element of an array into a new form.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="source"></param>
		/// <param name="selector">A transform function to apply to each element.</param>
		/// <returns></returns>
		public static Array Map<T, TResult>(this Array source, Func<T, TResult> selector)
		{
			if (source == null)
				throw new ArgumentNullException("source");

			if (selector == null)
				throw new ArgumentNullException("selector");

			int[] lengths = source.GetLengths();
			Array result = Array.CreateInstance(typeof(TResult), lengths);

			// project each element into a new form
			foreach (int[] indices in Range(lengths))
				result.SetValue(selector((T)result.GetValue(indices)), indices);

			return result;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="source"></param>
		/// <param name="predicate"></param>
		/// <param name="replacement"></param>
		/// <returns></returns>
		public static Array Replace<T>(this Array source, Func<T, bool> predicate, T replacement)
		{
			return source.Map<T, T>(e => predicate(e) ? replacement : e);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="source"></param>
		/// <param name="subject"></param>
		/// <param name="replacement"></param>
		/// <returns></returns>
		public static Array Replace<T>(this Array source, T subject, T replacement) where T : IEquatable<T>
		{
			return source.Replace(e => e.Equals(subject), replacement);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="source"></param>
		/// <param name="predicate"></param>
		/// <returns></returns>
		public static Array Where<T>(this Array source, Func<T, bool> predicate)
		{
			return source.Map<T, T>(e => predicate(e) ? e : default(T));
		}
		#endregion

		#region Flip
		/// <summary>
		/// Flips the elements of an array at a specified dimension.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="source"></param>
		/// <param name="dimension"></param>
		/// <returns></returns>
		public static Array Flip(this Array source, int dimension)
		{
			if (source == null)
				throw new ArgumentNullException("source");

			if (dimension < 0 || dimension >= source.Rank)
				throw new ArgumentOutOfRangeException("dimension");

			int[] lengths = source.GetLengths();
			Array result = Array.CreateInstance(source.GetType().GetElementType(), lengths);

			foreach (int[] indices in Range(lengths))
			{
				// transform indicies
				int[] transformed = indices.Clone() as int[];
				transformed[dimension] = lengths[dimension] - indices[dimension] - 1;

				// set value
				result.SetValue(result.GetValue(transformed), indices);
			}

			return result;
		}
		#endregion

		#region Subarrays
		/// <summary>
		/// 
		/// </summary>
		/// <param name="source"></param>
		/// <param name="startIndicies"></param>
		/// <param name="lengths"></param>
		/// <returns></returns>
		public static Array Crop(this Array source, int[] startIndicies, int[] lengths)
		{
			if (source == null)
				throw new ArgumentNullException("source");

			if (startIndicies == null)
				throw new ArgumentNullException("startIndicies");

			if (startIndicies.Length != source.Rank)
				throw new ArgumentException("startIndicies");

			if (lengths == null)
				throw new ArgumentNullException("lengths");

			if (lengths.Length != source.Rank)
				throw new ArgumentException("lengths");

			if (lengths.Any(i => i <= 0))
				throw new ArgumentException("Dimension lengths must be positive integers.", "lengths");

			int[] sourceLengths = source.GetLengths();
			Array result = Array.CreateInstance(source.GetType().GetElementType(), lengths);

			foreach (int[] indices in Range(lengths))
			{
				int[] shiftedIndicies = new int[indices.Length];

				for (int i = 0; i < indices.Length; i++)
					shiftedIndicies[i] = startIndicies[i] + indices[i];

				result.SetValue(source.GetValue(shiftedIndicies), indices);
			}

			return result;
		}

		/// <summary>
		/// Resizes an array to a specified size.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="source"></param>
		/// <param name="newLengths"></param>
		/// <param name="paddingElement"></param>
		/// <returns></returns>
		public static Array Resize<T>(this Array source, int[] newLengths, T paddingElement = default(T))
		{
			if (source == null)
				throw new ArgumentNullException("source");

			if (newLengths == null)
				throw new ArgumentNullException("newLengths");

			if (newLengths.Length != source.Rank)
				throw new ArgumentException("newLengths");

			if (newLengths.Any(i => i <= 0))
				throw new ArgumentException("Dimension lengths must be positive integers.", "newLengths");

			int[] lengths = source.GetLengths();
			Array result = Array.CreateInstance(typeof(T), newLengths);

			foreach (int[] indices in Range(newLengths))
			{
				bool inBounds = true;

				for (int i = 0; i < indices.Length; i++)
				{
					if (indices[i] >= lengths[i])
					{
						inBounds = false;
						continue;
					}
				}

				result.SetValue(inBounds ? source.GetValue(indices) : paddingElement, indices);
			}

			return result;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="source"></param>
		/// <param name="indiciesToSkip"></param>
		/// <returns></returns>
		public static Array MinorMatrix(this Array source, params int[] indiciesToSkip)
		{
			if (source == null)
				throw new ArgumentNullException("source");

			if (indiciesToSkip == null)
				throw new ArgumentNullException("coordinates");

			int[] lengths = source.GetLengths();

			if (indiciesToSkip.Length != source.Rank)
				throw new ArgumentException("coordinates");

			int[] newLengths = new int[source.Rank];

			for (int i = 0; i < source.Rank; i++)
			{
				if (lengths[i] <= 1)
					throw new ArgumentException("source");

				if (indiciesToSkip[i] >= lengths[i])
					throw new ArgumentOutOfRangeException("coordinates");

				newLengths[i] = lengths[i] - 1;
			}

			Array result = Array.CreateInstance(source.GetType().GetElementType(), newLengths);

			foreach (int[] indices in Range(newLengths))
			{
				int[] projectedIndicies = indices.Clone() as int[];

				bool @continue = false;

				for (int i = 0; i < indices.Length; i++)
				{
					// skip if needed
					if (projectedIndicies[i] == indiciesToSkip[i])
					{
						@continue = true;
						break;
					}

					// align indicies
					if (projectedIndicies[i] > indiciesToSkip[i])
						projectedIndicies[i]--;
				}

				if (@continue)
					continue;

				result.SetValue(source.GetValue(indices), projectedIndicies);
			}

			return result;
		}
		#endregion

		#region Combination
		/// <summary>
		/// Merges two arrays by using the specified predicate function.
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <typeparam name="T2"></typeparam>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="source"></param>
		/// <param name="second">The second array to merge.</param>
		/// <param name="resultSelector">A function that specifies how to merge the elements from the two arrays.</param>
		/// <returns></returns>
		public static Array Combine<T1, T2, TResult>(this Array source, this Array second, Func<T1, T2, TResult> resultSelector)
		{
			if (source == null)
				throw new ArgumentNullException("source");

			if (second == null)
				throw new ArgumentNullException("second");

			if (resultSelector == null)
				throw new ArgumentNullException("resultSelector");

			if (source.Rank != second.Rank)
				throw new ArgumentException("The rank of the second array is not equal to the rank of the first array.", "second");

			int[] lengths = source.GetLengths();
			
			// check dimension lengths
			int[] secondLengths = second.GetLengths();

			for (int i = 0; i < lengths.Length; i++)
				if (lengths[i] != secondLengths[i])
					throw new ArgumentException("Dimension lengths are not equal.", "second");

			// create result container
			Array result = Array.CreateInstance(typeof(TResult), lengths);

			foreach (int[] indices in Range(lengths))
			{
				// select results from each pair
				result.SetValue(resultSelector((T1)source.GetValue(indices), (T2)second.GetValue(indices)));
			}

			return result;
		}
		#endregion

		/// <summary>
		/// Determines whether two arrays are equal by comparing their elements using an equality comparer.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="source"></param>
		/// <param name="second">The array to compare.</param>
		/// <param name="comparer">An equality comparer to use to compare elements.</param>
		/// <returns></returns>
		public static bool ArrayEqual<T>(this Array source, Array second, IEqualityComparer<T> comparer)
		{
			if (source == null)
				throw new ArgumentNullException("source");

			if (second == null)
				throw new ArgumentNullException("second");

			if (comparer == null)
				throw new ArgumentNullException("comparer");

			int[] lengths = source.GetLengths();
			int[] secondLengths = second.GetLengths();

			for (int i = 0; i < lengths.Length; i++)
				if (lengths[i] != secondLengths[i])
					return false;

			foreach (int[] indices in Range(lengths))
			{
				// test each element using an equality comparer
				if (!comparer.Equals((T)source.GetValue(indices), (T)second.GetValue(indices)))
					return false;
			}

			return true;
		}
		/// <summary>
		/// Determines whether two arrays are equal by comparing their elements.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="source"></param>
		/// <param name="second">The array to compare.</param>
		/// <returns></returns>
		public static bool ArrayEqual<T>(this Array source, Array second)
		{
			if (source == null)
				throw new ArgumentNullException("source");

			if (second == null)
				throw new ArgumentNullException("second");

			if (source.Rank != second.Rank)
				return false;

			int[] lengths = source.GetLengths();
			int[] secondLengths = second.GetLengths();

			for (int i = 0; i < lengths.Length; i++)
				if (lengths[i] != secondLengths[i])
					return false;

			foreach (int[] indices in Range(lengths))
			{
				// test each element
				if (!source.GetValue(indices).Equals(second.GetValue(indices)))
					return false;
			}

			return true;
		}
	}
}
